spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { Confidence, EntityLinks, EventTypeBadge, entityHref, entityName, eventLabel } from '@/components/events/event-badge';5import { Note } from '@/components/stats/shared';6import { Container, PageHeader, Section } from '@/components/ui/section';7import { Unavailable } from '@/components/ui/unavailable';8import { api, ApiError } from '@/lib/api';9import { fmtAgo, fmtDateTime } from '@/lib/format';10import { routes, SITE_NAME, SITE_URL } from '@/lib/site';11import type { EventRow } from '@/lib/types';1213export const revalidate = 300;1415type Params = Promise<{ id: string }>;1617async function load(id: string): Promise<EventRow | null | 'unavailable'> {18 try {19 return (await api.event(id)).data;20 } catch (e) {21 if (e instanceof ApiError && e.notFound) return null;22 return 'unavailable';23 }24}2526export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {27 const { id } = await params;28 const ev = await load(id);29 const url = `${SITE_URL}/events/${encodeURIComponent(id)}`;30 if (!ev || ev === 'unavailable') return { title: 'Event', alternates: { canonical: url }, robots: { index: false } };31 const title = `${ev.title} — ${eventLabel(ev.type)}`;32 const description = ev.summary ? `${ev.summary.slice(0, 180)}${ev.summary.length > 180 ? '…' : ''}` : `${eventLabel(ev.type)} event detected ${fmtDateTime(ev.event_time)} (source: ${ev.source_name ?? ev.source_id ?? 'unknown'}).`;33 return {34 title,35 description,36 alternates: { canonical: url },37 openGraph: { title: `${title} | ${SITE_NAME}`, description, url, type: 'article', siteName: SITE_NAME, publishedTime: ev.event_time },38 twitter: { card: 'summary_large_image', title: `${title} | ${SITE_NAME}`, description },39 };40}4142function Field({ label, children, mono = false }: { label: string; children: React.ReactNode; mono?: boolean }) {43 return (44 <div className="border-t border-rule py-3">45 <p className="eyebrow">{label}</p>46 <div className={`mt-1 text-sm text-ink ${mono ? 'mono break-all' : ''}`}>{children}</div>47 </div>48 );49}5051export default async function EventDetailPage({ params }: { params: Params }) {52 const { id } = await params;53 const ev = await load(id);54 if (ev === null) notFound();55 if (ev === 'unavailable') {56 return (57 <Container>58 <PageHeader eyebrow="Event" title="Event" />59 <Unavailable what="Event detail" />60 </Container>61 );62 }63 const entities = ev.entities ?? [];64 const jsonLd = {65 '@context': 'https://schema.org',66 '@type': 'Event',67 name: ev.title,68 description: ev.summary ?? undefined,69 startDate: ev.event_time,70 eventStatus: 'https://schema.org/EventScheduled',71 location: { '@type': 'Place', name: 'Earth orbit' },72 url: `${SITE_URL}/events/${encodeURIComponent(ev.id)}`,73 };7475 return (76 <Container>77 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />78 <PageHeader79 eyebrow={80 <span className="inline-flex flex-wrap items-center gap-2">81 <Link href={routes.events()} className="hover:text-accent">Events</Link>82 <span aria-hidden>/</span>83 <EventTypeBadge type={ev.type} />84 </span>85 }86 title={ev.title}87 lede={ev.summary ?? undefined}88 >89 <p className="mono mt-4 text-xs text-ink-3">90 <time dateTime={ev.event_time}>{fmtDateTime(ev.event_time)}</time> · {fmtAgo(ev.event_time)}91 {ev.detected_at && <> · detected {fmtAgo(ev.detected_at)}</>}92 </p>93 </PageHeader>9495 <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_360px] lg:gap-14">96 <div className="space-y-10">97 <Section eyebrow="Entities" title={`${entities.length} linked ${entities.length === 1 ? 'entity' : 'entities'}`} className="pt-0 md:pt-0">98 {entities.length === 0 ? (99 <Unavailable what="Linked entities" compact />100 ) : (101 <table className="data-table stack text-sm">102 <thead>103 <tr>104 <th>Type</th>105 <th>Entity</th>106 <th>Relationship</th>107 <th>NORAD</th>108 </tr>109 </thead>110 <tbody>111 {entities.map((e) => {112 const href = entityHref(e);113 return (114 <tr key={`${e.type}-${e.id}`}>115 <td data-label="Type" className="mono text-xs uppercase text-ink-3">{e.type.replace('_', ' ')}</td>116 <td data-label="Entity" className="primary">117 {href ? <Link href={href} className="link font-medium">{entityName(e)}</Link> : <span>{entityName(e)}</span>}118 {e.type === 'launch' && e.name && <span className="ml-2 text-xs text-ink-3">{e.name}</span>}119 </td>120 <td data-label="Relationship" className="text-ink-2">{e.relationship ?? '—'}</td>121 <td data-label="NORAD" className="mono text-xs">{e.norad_id ?? '—'}</td>122 </tr>123 );124 })}125 </tbody>126 </table>127 )}128 <EntityLinks entities={entities} className="mt-3 md:hidden" max={4} />129 </Section>130131 <Section eyebrow="Metadata" title="Raw event metadata" className="pt-0 md:pt-0">132 {ev.metadata && Object.keys(ev.metadata).length ? (133 <pre className="mono scrollbar-thin overflow-x-auto rounded-md border border-rule bg-plane p-4 text-xs leading-relaxed text-ink-2">{JSON.stringify(ev.metadata, null, 2)}</pre>134 ) : (135 <Unavailable what="Metadata" compact />136 )}137 <Note className="mt-3">Metadata is stored exactly as produced by the connector that detected the event; keys vary by event type.</Note>138 </Section>139 </div>140141 <aside className="lg:sticky lg:top-[calc(var(--header-h)+1rem)] lg:self-start">142 <p className="eyebrow mb-1">Telemetry</p>143 <Field label="Event id" mono>{ev.id}</Field>144 <Field label="Type">145 <EventTypeBadge type={ev.type} size="md" />146 <span className="mono ml-2 text-xs text-ink-3">{ev.type}</span>147 </Field>148 <Field label="Event time" mono>{fmtDateTime(ev.event_time)}</Field>149 {ev.detected_at && <Field label="Detected at" mono>{fmtDateTime(ev.detected_at)}</Field>}150 <Field label="Source">151 {ev.source_url ? (152 <a href={ev.source_url} target="_blank" rel="noopener noreferrer" className="link">{ev.source_name ?? ev.source_id}</a>153 ) : (154 <span>{ev.source_name ?? ev.source_id ?? 'Unavailable'}</span>155 )}156 {ev.source_id && <span className="mono ml-2 text-xs text-ink-3">{ev.source_id}</span>}157 </Field>158 <Field label="Confidence">159 <Confidence value={ev.confidence} className="text-base" />160 <span className="ml-2 text-xs text-ink-3">161 derived · <Link href={routes.methodology()} className="hover:text-accent">methodology</Link>162 </span>163 </Field>164 <div className="border-t border-rule pt-3">165 <Link href={routes.events(`type=${encodeURIComponent(ev.type)}`)} className="text-sm text-accent hover:underline">All {eventLabel(ev.type).toLowerCase()} events →</Link>166 </div>167 </aside>168 </div>169 </Container>170 );171}172